#encoding:utf-8
import tornado.ioloop
import tornado.web
import pystache
import tornado.gen as gen
from logzero import logger as log
from concurrent.futures import ThreadPoolExecutor
import time
import signal
import ujson
from torutil import is_mobile

executor_g = ThreadPoolExecutor()

class WebApplication(tornado.web.Application):
    def __init__(self,*opts,**args):
        super(WebApplication,self).__init__(*opts,**args)
        self.manage={}
        self.__render = pystache.Renderer(
            search_dirs=args.get('search_dirs','./'), 
            file_extension=args.get('file_extension','html'),
            file_encoding=args.get('file_encoding','utf-8'), 
            string_encoding=args.get('string_encoding','utf-8')
        )
        # 不好用，stop后还需要等待下一次事件循环
        # signal.signal(signal.SIGTERM, self.shutdown)
        # signal.signal(signal.SIGINT, self.shutdown)
        # signal.signal(signal.SIGILL, self.shutdown)

    def render_text(self, text, *context, **args):
        return self.__render.render(text, *context, **args)

    def render_name(self, template_name, *context, **args):
        return self.__render.render_name(template_name, *context, **args)

    def shutdown(self,sig, frame):
        log.warning('Caught signal: %s', sig)
        tornado.ioloop.IOLoop.instance().stop()
        log.info('Shutdown Server......')

    def route(self, url,role=[],auth=True,method=['POST','GET'],log_func=None,mobile_url=None,token=False,ip=None,name=None,code=None,parent=[],alias=[]):
        '''
        app = WebApplication(debug=True,login_url='/',file_extension='html',file_encoding='utf-8')

        1、登录校验：
            前提——需要配置login_url参数：app = WebApplication(login_url='/')。
                  需要重写get_current_user，返回当前登录用户信息。
            默认——开启状态，对于登录、退出、首页等无需校验的url需要手动关闭检验：auth=False
            运行时设置——无
            样例：
            @app.route('/login',auth=False)
            class SayHandler(MainHandler):
                def get(self):
                    self.render_text('<h1>main - {{template}}</h1>',{'template':txt})
        2、角色控制：
            前提——重写get_current_role，返回登录用户角色名称。
            默认——不进行控制，默认值是空列表。
            运行时设置——重写MainHandler.refresh_role(self)，将动态的角色控制逻辑整理好，该方法将在运行时替换
                装饰器的参数方式，也就是说如果重写了refresh_role那么优先进行处理。
            样例：
            @app.route('/login',role=['admin'])
            class SayHandler(MainHandler):
                def get(self):
                    self.render_text('<h1>main - {{template}}</h1>',{'template':txt})
        3、方法控制：
            前提——无
            默认——包含POST,GET
            运行时设置——无
            样例：
            @app.route('/login',method=['GET'])
            class SayHandler(MainHandler):
                def get(self):
                    self.render_text('<h1>main - {{template}}</h1>',{'template':txt})
        4、日志控制：
            前提——需要准备一个函数，参数有3个，第一个为handler的名称；第二个是request对象;第三个是当前请求的http status。
            默认——None，不进行日志记录
            运行时设置——无
            样例：
            @app.route('/login',log_func=log_to_mongodb)
            class SayHandler(MainHandler):
                def get(self):
                    self.render_text('<h1>main - {{template}}</h1>',{'template':txt})
        5、移动控制：
            前提：无
            默认：不区分pc/手机版，当设置此值后，如果监测到客户端是手机版将重定向到指定mobile_url处。
            运行时设置：无
            样例：
            @app.route('/login',mobile_url='/mobile_login')
            class SayHandler(MainHandler):
                def get(self):
                    self.render_text('<h1>main - {{template}}</h1>',{'template':txt})
        6、token控制：
            前提：参数中必须要有"token"参数
            默认：不进行控制。
            运行时设置：重写MainHandler.refresh_tokens(self)，要求返回一个包含有效token的list。
            样例：
            @app.route('/login',token=True)
            class SayHandler(MainHandler):
                def refresh_tokens(self):
                    return ['1','2']
                def get(self):
                    self.render_text('<h1>main - {{template}}</h1>',{'template':txt})
        7、ip控制：
            前提：无
            默认：不进行控制。必须设置为list，限定指定ip才可以访问此接口。
            运行时设置：无
            样例：
            @app.route('/login',ip=['127.0.0.1'])
            class SayHandler(MainHandler):
                def get(self):
                    self.render_text('<h1>main - {{template}}</h1>',{'template':txt})
        8、code标识：
            前提：无
            默认：不进行控制。必须设置为str。
            运行时设置：无
            样例：
            @app.route('/login',code='home')
            class SayHandler(MainHandler):
                def get(self):
                    self.render_text('<h1>main - {{template}}</h1>',{'template':txt})
        9、name标识：
            前提：必须同时开启code参数
            默认：不进行控制。必须设置为str。
            运行时设置：请求中自动添加'_{code}_name'=name
            样例：
            @app.route('/login',code='home',name='Home Page')
            class SayHandler(MainHandler):
                def get(self):
                    self.render_text('<h1>main - {{template}}</h1>',{'template':txt})
        10、parent标识：
            前提：所有父节点必须同时开启code、name参数
            默认：不进行控制。必须设置为list，内容为父节点code值，如果某节点是多个页面可以使用list来代替。例如：
            ['home',['age','sex'],'info']，这时需要_bread_参数选择下标为0还是1作为结果返回。如果有多个list则
            _bread_参数使用逗号分隔。
            运行时设置：请求中自动添加所有父节点的name列表'_{code}_parent'=[{'name':,'url':},...]。
            样例：
            @app.route('/login',code='search',name='Search',parent=['home'])
            class SayHandler(MainHandler):
                def get(self):
                    self.render_text('<h1>main - {{template}}</h1>',{'template':txt})
        11、alias标识：
            前提：无
            默认：不进行控制。必须设置为list。
            运行时设置：自动生成请求别名
            样例：
            @app.route('/login',alias=['/login_main'])
            class SayHandler(MainHandler):
                def get(self):
                    self.render_text('<h1>main - {{template}}</h1>',{'template':txt})
        '''
        def register(handler):
            self.add_handlers(".*$", [(url, handler)]) # URL和Handler对应关系添加到路由表中
            if alias:
                for uri in alias:
                    self.add_handlers(".*$", [(uri, handler)])
            params={'url':url,'role':role,'auth':auth,'method':[x.upper() for x in method],'log_func':log_func,'mobile_url':mobile_url,'token':token,'ip':ip,'name':name,'code':code,'parent':parent,'alias':alias}
            self.manage[handler.__name__]=params
            if code:
                self.manage[f'code_{code}']=params
            return handler
        return register

class MainHandler(tornado.web.RequestHandler):
    def handler_meta(self,code):
        return self.application.manage.get(f'code_{code}',{})
        
    def refresh_role(self):
        return []
    def refresh_tokens(self):
        return []

    def before_exec(self):
        '''
        :returns: url or None
        '''
        pass
    def after_exec(self):
        '''
        :returns: url or None
        '''
        pass

    @gen.coroutine
    def prepare(self):
        v=self.before_exec()
        if self.get_status()!=200:
            return # 允许before_exec设置返回状态，同时禁止后续的请求修改，避免报错。
        if v:
            self.redirect(v)
            return
        manage=self.application.manage.get(self.__class__.__name__)
        if manage:
            if manage['auth'] and not self.get_current_user():
                self.redirect(self.get_login_url())
                return
            t_token=yield executor_g.submit(self.refresh_tokens)
            if manage['token'] and self.get_argument('token','') not in t_token:
                self.send_error(406)
                return
            if manage['ip'] and self.request.remote_ip not in manage['ip']:
                self.send_error(406)
                return
            if manage['mobile_url'] and is_mobile(self.request):
                self.redirect(manage['mobile_url'])
                return
            if manage['method'] and self.request.method.upper() not in manage['method']:
                self.send_error(403)
                return
            t_role=yield executor_g.submit(self.refresh_role)
            if t_role and self.get_current_role() not in t_role:
                self.send_error(406)
                return
            if manage['role'] and self.get_current_role() not in manage['role']:
                self.send_error(406)
                return
        v=self.after_exec()
        if v:
            self.redirect(v)
            return

    @gen.coroutine
    def on_finish(self):
        manage=self.application.manage.get(self.__class__.__name__)
        if manage and manage['log_func']:# 任何http-status都需要日志，所以删除了200的限定。
            yield executor_g.submit(manage['log_func'],self.__class__.__name__,self.request,self.get_status())

    def get_current_user(self):
        return None

    def get_current_role(self):
        return None

    def sys_render(self):
        manage=self.application.manage.get(self.__class__.__name__,{})
        code=manage.get('code','_uncode_')
        parent=[]
        index=[x for x in self.get_argument('_bread_','').split(',')]
        for x in manage.get('parent',[]):
            if type(x)==list:#处理并列的节点
                i=0
                try:
                    i=int(index.pop())
                except:
                    pass
                x1=x[i]
                obj=self.application.manage.get(f'code_{x1}',{})
                if obj:
                    parent.append({'name': obj.get('name',''),'url':obj.get('url','#')})
            else:
                obj=self.application.manage.get(f'code_{x}',{})
                parent.append({'name': obj.get('name',''),'url':obj.get('url','#')})
        
        v={
            f'_{code}_active':True,
            f'_{code}_url':manage.get('url','#'),
            f'_{code}_name':manage.get('name',''),
            f'_{code}_parent':parent+[{'name':manage.get('name',''),'url':manage.get('url','#')}],
            '_menus':[{'name':v.get('name',''),'url':v.get('url','#'),'active':v.get('code') in [code]+manage.get('parent',[])} for k,v in self.application.manage.items() if k.startswith('code_') and not v.get('parent')]
        }
        return v

    def before_render(self):
        '''
        :returns: dict or None
        '''
        pass

    def render_text(self, text, *context, **args):
        tmp=self.before_render()
        if type(tmp)==dict: args.update(tmp)
        tmp=self.sys_render()
        if type(tmp)==dict: args.update(tmp)
        self.write(self.application.render_text(text,*context,**args))

    def render_name(self, template_name, *context, **args):
        tmp=self.before_render()
        if type(tmp)==dict: args.update(tmp)
        tmp=self.sys_render()
        if type(tmp)==dict: args.update(tmp)
        self.write(self.application.render_name(template_name,*context,**args))

    def get_cookie_value(self,key,default=None):
        return self.get_secure_cookie(key) or default

    def set_cookie_value(self,key,value):
        self.set_secure_cookie(key,value)

    def params(self,*names,default=None):
        if type(default)!=list:
            default=[default for _ in range(len(names))]
        return map(self.get_argument,names,default)

    def to_json(self,txt):
        self.set_header('Content-Type', 'application/json; charset=UTF-8')
        self.write(ujson.dumps(txt))


if __name__=='__main__':
    app = WebApplication(cookie_secret='dddddd',xheaders=True,debug=True,login_url='/',file_extension='html',file_encoding='utf-8')
    import random,time
    class BaseHandler(MainHandler):
        pass
        
    # @app.route('/',role=[],auth=False,token=True,method=['POST','GET'],log_func=None)
    class SayHandler(tornado.web.RequestHandler):
        def get(self):
            txt=self.get_argument('t','Say...')
            # print(self.get_cookie_value('userid','yuer'))
            self.write('<h1>index - {template}</h1>'.format(template=txt))
        def refresh_role(self):
            return []

    @app.route('/hi',role=[],auth=False,token=False,method=['POST','GET'],log_func=None)
    class HelloHandler(BaseHandler):
        @gen.coroutine
        def get(self):
            txt=self.get_argument('t','Hello...')
            self.write('dddddddd')
            '''
            /hi?t=test<svg/onload=alert(1)> - {{{template}}} - <svg onload="alert(1)"></svg>
            /hi?t=test"><svg/onload=alert(1)> - <input value="{{{template}}}"> -<input value="test"><svg onload="alert(1)">"&gt;</svg>
            '''
            self.render_text('<h1>main - {{{template}}}</h1>{{template}}<script>{{{template}}};{{template}};</script><br><input value="{{{template}}}">',{'template':txt})
        def refresh_role(self):
            return []
    app.add_handlers(".*$",[( r'/', SayHandler )])
    app.listen(9527)
    tornado.ioloop.IOLoop.current().start()